/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.keycloak.social.dingtalk;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;

import org.apache.http.impl.client.HttpClientBuilder;
import org.keycloak.OAuth2Constants;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.messages.Messages;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * 
 * @author yong.jiang
 */
public class DingtalkIdentityProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig>
		implements SocialIdentityProvider<OAuth2IdentityProviderConfig> {

	// ?appid=APPID&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI
	public static final String AUTH_URL = "https://oapi.dingtalk.com/connect/qrconnect";
	// ?appid=APPID&response_type=code&scope=snsapi_auth&state=STATE&redirect_uri=REDIRECT_URI
	public static final String DINGTALK_AUTH_URL = "https://oapi.dingtalk.com/connect/oauth2/sns_authorize";
	// ?appid=APPID&appsecret=APPSECRET
	public static final String TOKEN_URL = "https://oapi.dingtalk.com/sns/gettoken";
	// ?access_token=ACCESS_TOKEN
	public static final String GET_PERSISTENT_CODE_URL = "https://oapi.dingtalk.com/sns/get_persistent_code";
	// ?access_token=ACCESS_TOKEN
	public static final String SNS_TOKEN_URL = "https://oapi.dingtalk.com/sns/get_sns_token";
	// ?sns_token=SNS_TOKEN
	public static final String PROFILE_URL = "https://oapi.dingtalk.com/sns/getuserinfo";
	// ?access_token=ACCESS_TOKEN&unionid=xxx
	public static final String GET_USERID_URL = "https://oapi.dingtalk.com/user/getUseridByUnionid";

	public static final String DEFAULT_SCOPE = "snsapi_login";
	public static final String OAUTH2_PARAMETER_CLIENT_ID = "appid";
	public static final String OAUTH2_PARAMETER_CLIENT_SECRET = "appsecret";

	public static final String CORPID = "corpid";
	public static final String CORPSECRET = "corpsecret";

	// 企业应用服务端获取token
	// ?corpid=id&corpsecret=secret
	public static final String CORP_TOKEN_URL = "https://oapi.dingtalk.com/gettoken";

	public static final String GET_USER_DETAIL_URL = "https://oapi.dingtalk.com/user/get?access_token=ACCESS_TOKEN&userid=zhangsan";

	public DingtalkIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
		super(session, config);
		config.setAuthorizationUrl(AUTH_URL);
		config.setTokenUrl(TOKEN_URL);
		config.setUserInfoUrl(PROFILE_URL);
	}

	@Override
	public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
		return new Endpoint(callback, realm, event);
	}

	@Override
	protected boolean supportsExternalExchange() {
		return true;
	}

	@Override
	protected String getProfileEndpointForValidation(EventBuilder event) {
		return PROFILE_URL;
	}

	/**
	 * 构造登录跳转地址
	 */
	@Override
	protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {

		final UriBuilder uriBuilder = UriBuilder.fromUri(getConfig().getAuthorizationUrl());
		uriBuilder.queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
				.queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncoded())
				.queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId()).queryParam("response_type", "code")
				.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());

		String loginHint = request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
		if (getConfig().isLoginHint() && loginHint != null) {
			uriBuilder.queryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint);
		}

		String prompt = getConfig().getPrompt();
		if (prompt == null || prompt.isEmpty()) {
			prompt = request.getAuthenticationSession().getClientNote(OAuth2Constants.PROMPT);
		}
		if (prompt != null) {
			uriBuilder.queryParam(OAuth2Constants.PROMPT, prompt);
		}

		String nonce = request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.NONCE_PARAM);
		if (nonce == null || nonce.isEmpty()) {
			nonce = UUID.randomUUID().toString();
			request.getAuthenticationSession().setClientNote(OIDCLoginProtocol.NONCE_PARAM, nonce);
		}
		uriBuilder.queryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);

		String acr = request.getAuthenticationSession().getClientNote(OAuth2Constants.ACR_VALUES);
		if (acr != null) {
			uriBuilder.queryParam(OAuth2Constants.ACR_VALUES, acr);
		}
		return uriBuilder;
	}

	/**
	 * 设置关联属性
	 */
	@Override
	protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
		BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "unionid"));
		String unionid = getJsonProperty(profile, "unionid");
		String userid = getJsonProperty(profile, "userid");
		String email = getJsonProperty(profile, "email");
		String mobile = getJsonProperty(profile, "mobile");
		user.setUsername(userid);
		if (user.getUsername() == null) {
			user.setUsername(email);
		}
		if (user.getUsername() == null) {
			user.setUsername(mobile);
		}
		if (user.getUsername() == null) {
			user.setUsername(unionid);
		}
		user.setFirstName(conversionCharacter(getJsonProperty(profile, "nick"), "UTF-8"));
		user.setEmail(email);
		user.setIdpConfig(getConfig());
		user.setIdp(this);

		/**
		 * 设置额外属性
		 */
		String extAtts = getConfig().getConfig().get("extAttrs");
		if (extAtts == null || extAtts.length() <= 0) {
			extAtts = "mobile,position,age";
		}
		for (String attr : extAtts.split(",")) {
			// 获取额外属性
			if (profile.has("extattr") && !profile.get("extattr").isNull()) {
				user.setUserAttribute(attr,
						conversionCharacter(getJsonProperty(profile.get("extattr"), attr), "UTF-8"));
			}
			// 原始属性优先
			user.setUserAttribute(attr, conversionCharacter(getJsonProperty(profile, attr), "UTF-8"));
		}

		// 是否显示额外属性
		if (userid != null && userid.length() > 0) {
			user.setFirstName(conversionCharacter(getJsonProperty(profile, "name"), "UTF-8"));
			user.setUserAttribute("idpType", DingtalkIdentityProviderFactory.PROVIDER_ID);
		}

		AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
		return user;
	}

	/**
	 * 解析用户对象
	 */
	public BrokeredIdentityContext getFederatedIdentity(String response) {
		BrokeredIdentityContext context = null;
		try {
			JsonNode profile = new ObjectMapper().readTree(response);
			logger.info("get userInfo =" + profile.toString());
			context = extractIdentityFromProfile(null, profile);
		} catch (IOException e) {
			logger.error(e);
		}
		return context;
	}

	/**
	 * 获取 accessToken
	 * 
	 * @return
	 * @throws Exception
	 */
	public String getAppAccesstoken() throws Exception {
		JsonNode profile = SimpleHttp.doGet(TOKEN_URL, session).header("Accept", "application/json;charset=utf-8")
				.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
				.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asJson();
		String accessToken = getJsonProperty(profile, "access_token");
		logger.info("getAppAccesstoken=" + profile);
		return accessToken;
	}

	/**
	 * 获取 PersistentCode
	 * 
	 * @param accessToken
	 *            开发应用获取accessToken
	 * @param code
	 *            回调路径里面所带code
	 * @return
	 * @throws Exception
	 */
	public JsonNode getPersistentCode(String accessToken, String code) throws Exception {
		Map<String, String> param = new HashMap<String, String>();
		param.put("tmp_auth_code", code);
		JsonNode profile = SimpleHttp.doPost(GET_PERSISTENT_CODE_URL + "?access_token=" + accessToken, session)
				.header("Accept", "application/json;charset=utf-8").json(param).asJson();
		logger.info("getPersistentCode=" + profile);
		return profile;
	}

	/**
	 * 获取用户信息授权 sns_token
	 * 
	 * @param accessToken
	 * @param openid
	 *            用户的openid
	 * @param persistentCode
	 * @return
	 * @throws Exception
	 */
	public String getSnsToken(String accessToken, String openid, String persistentCode) throws Exception {
		Map<String, String> param = new HashMap<String, String>();
		param.put("openid", openid);
		param.put("persistent_code", persistentCode);
		JsonNode profile = SimpleHttp.doPost(SNS_TOKEN_URL + "?access_token=" + accessToken, session).json(param)
				.header("Accept", "application/json;charset=utf-8").asJson();
		String tmpAuthCode = getJsonProperty(profile, "sns_token");
		logger.info("getSnsToken" + profile);
		return tmpAuthCode;
	}

	/**
	 * 获取用户信息
	 * 
	 * @param sns_token
	 * @return
	 * @throws Exception
	 */
	public JsonNode getUserInfo(String sns_token) throws Exception {
		JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).param("sns_token", sns_token)
				.header("Accept", "application/json;charset=utf-8").asJson();
		logger.info("getUserInfo=" + profile);
		return profile;
	}

	/**
	 * 企业内部开发获取access_token
	 * 
	 * @return
	 * @throws Exception
	 */
	public String getCorpAccesstoken() throws Exception {
		JsonNode profile = SimpleHttp.doGet(CORP_TOKEN_URL, session).header("Accept", "application/json;charset=utf-8")
				.param(CORPID, getConfig().getConfig().get(CORPID))
				.param(CORPSECRET, getConfig().getConfig().get(CORPSECRET)).asJson();
		String accessToken = getJsonProperty(profile, "access_token");
		logger.info("getAppAccesstoken=" + profile);
		return accessToken;
	}

	/**
	 * 获取用户id
	 * 
	 * @param accessToken
	 *            调用接口凭证
	 * @param unionid
	 * @return
	 * @throws Exception
	 */
	public String getUseridByUnionid(String accessToken, String unionid) throws Exception {
		JsonNode profile = SimpleHttp.doGet(GET_USERID_URL, session).header("Accept", "application/json;charset=utf-8")
				.param("access_token", accessToken).param("unionid", unionid).asJson();
		logger.info("getUseridByUnionid=" + profile);
		String userid = getJsonProperty(profile, "userid");
		return userid == null ? "" : userid;
	}

	/**
	 * 获取用户详情
	 * 
	 * @param accessToken
	 * @param userid
	 * @return
	 * @throws Exception
	 */
	public JsonNode getUseridDetail(String accessToken, String userid) throws Exception {
		JsonNode profile = SimpleHttp.doGet(GET_USER_DETAIL_URL, session)
				.header("Accept", "application/json;charset=utf-8").param("access_token", accessToken)
				.param("userid", userid).asJson();
		logger.info("getUseridDetail=" + profile);
		return profile;
	}

	private static String conversionCharacter(String source, String encoding) {
		if (source == null || source.length() <= 0) {
			return "";
		}
		try {
			System.out.println(System.getProperty("file.encoding"));
			return new String(source.getBytes(), encoding);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "";
	}

	public static void main(String[] args) throws IOException {

		JsonNode profile = SimpleHttp.doGet(CORP_TOKEN_URL, HttpClientBuilder.create().build())
				.header("Accept", "application/json;charset=ISO-8859-1")
				.param(CORPID, "ding856b118b5459600335c2f4657eb6378f")
				.param(CORPSECRET, "ZC8BZDAliXm77ikBy5eCO_3wJ1W49FJskUXZLGNGiw9JveDsMNQfNVK83ODRhnOO").asJson();
		logger.info("getAppAccesstoken=" + profile);

		String accessToken = profile.get("access_token").asText();// "1438eff8efa730d685ecfea6fbb82dd6";

		// JsonNode profile = SimpleHttp.doGet(GET_USERID_URL,
		// HttpClientBuilder.create().build())
		// .header("Accept",
		// "application/json;charset=utf-8").param("access_token", accessToken)
		// .param("unionid", "CkBazUSmBSQeGHLH2Y8GPwiEiE").asJson();
		// logger.info("getAppAccesstoken=" + profile);

		String userid = "manager2086";
		profile = SimpleHttp.doGet(GET_USER_DETAIL_URL, HttpClientBuilder.create().build())
				.header("Accept", "application/json;charset=UTF-8").param("access_token", accessToken)
				.param("userid", userid).asJson();
		logger.info("getUseridDetail=" + profile);
		String temp = profile.get("position").asText();
		logger.info("职位=" + temp);
		logger.info("职位=" + conversionCharacter(temp, "UTF-8"));

	}

	@Override
	protected String getDefaultScopes() {
		return DEFAULT_SCOPE;
	}

	protected class Endpoint {
		protected AuthenticationCallback callback;
		protected RealmModel realm;
		protected EventBuilder event;

		@Context
		protected KeycloakSession session;

		@Context
		protected ClientConnection clientConnection;

		@Context
		protected HttpHeaders headers;

		@Context
		protected UriInfo uriInfo;

		public Endpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
			this.callback = callback;
			this.realm = realm;
			this.event = event;
		}

		@GET
		public Response authResponse(@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_STATE) String state,
				@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_CODE) String authorizationCode,
				@QueryParam(OAuth2Constants.ERROR) String error) {
			logger.info("OAUTH2_PARAMETER_CODE=" + authorizationCode);
			if (error != null) {
				if (error.equals(ACCESS_DENIED)) {
					logger.error(ACCESS_DENIED + " for broker login " + getConfig().getProviderId());
					return callback.cancelled(state);
				} else {
					logger.error(error + " for broker login " + getConfig().getProviderId());
					return callback.error(state, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
				}
			}

			try {
				BrokeredIdentityContext federatedIdentity = null;
				if (authorizationCode != null) {
					// 1获取开发应用token
					String accessToken = getAppAccesstoken();

					// 2获取持久授权码和openid
					JsonNode persistentNode = getPersistentCode(accessToken, authorizationCode);

					// 3获取个人信息授权码
					String snsToken = getSnsToken(accessToken, getJsonProperty(persistentNode, "openid"),
							getJsonProperty(persistentNode, "persistent_code"));

					// 4 获取个人信息
					JsonNode userInfo = getUserInfo(snsToken);
					String response = userInfo.get("user_info").toString();

					// 5利用企业应用管理权限获取用户个人信息详情
					if (getConfig().getConfig().get(CORPID) != null
							&& getConfig().getConfig().get(CORPSECRET) != null) {

						if (userInfo != null && userInfo.get("user_info") != null
								&& userInfo.get("user_info").get("unionid") != null) {
							String cropAccessToken = getCorpAccesstoken();// 获取企业应用accesstoken
							String userId = getUseridByUnionid(cropAccessToken,
									userInfo.get("user_info").get("unionid").asText());// uuionid转化为userId
							if (userId != null && userId.length() > 1) {
								userInfo = getUseridDetail(cropAccessToken, userId);// 得到用户详情
								if (userInfo != null && "ok".equalsIgnoreCase(userInfo.get("errmsg").asText()))
									response = userInfo.toString();
							}

						}

					}

					logger.info("response=" + response);
					federatedIdentity = getFederatedIdentity(response);
					federatedIdentity.getContextData().put(FEDERATED_ACCESS_TOKEN, accessToken);

					if (getConfig().isStoreToken()) {
						if (federatedIdentity.getToken() == null)
							federatedIdentity.setToken(response);
					}

					federatedIdentity.setIdpConfig(getConfig());
					federatedIdentity.setIdp(DingtalkIdentityProvider.this);
					federatedIdentity.setCode(state);

					return callback.authenticated(federatedIdentity);
				}
			} catch (WebApplicationException e) {
				return e.getResponse();
			} catch (Exception e) {
				logger.error("Failed to make identity provider oauth callback", e);
			}
			event.event(EventType.LOGIN);
			event.error(Errors.IDENTITY_PROVIDER_LOGIN_FAILURE);
			return ErrorPage.error(session, null, Response.Status.BAD_GATEWAY,
					Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
		}
	}
}
